package com.powernode.map.service.impl;

import com.powernode.common.constant.RedisConstant;
import com.powernode.common.constant.SystemConstant;
import com.powernode.common.util.LocationUtil;
import com.powernode.driver.client.DriverInfoFeignClient;
import com.powernode.map.repository.OrderServiceLocationRepository;
import com.powernode.map.service.LocationService;
import com.powernode.model.entity.driver.DriverSet;
import com.powernode.model.entity.map.OrderServiceLocation;
import com.powernode.model.form.map.OrderServiceLocationForm;
import com.powernode.model.form.map.SearchNearByDriverForm;
import com.powernode.model.form.map.UpdateDriverLocationForm;
import com.powernode.model.form.map.UpdateOrderLocationForm;
import com.powernode.model.vo.map.NearByDriverVo;
import com.powernode.model.vo.map.OrderLocationVo;
import com.powernode.model.vo.map.OrderServiceLastLocationVo;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.bson.types.ObjectId;
import org.springframework.beans.BeanUtils;
import org.springframework.data.domain.Sort;
import org.springframework.data.geo.*;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.redis.connection.RedisGeoCommands;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;

/**
 * 位置服务实现类
 * 提供更新配送员位置信息到Redis地理空间索引和删除配送员位置信息的功能
 */
@Slf4j
@Service
@SuppressWarnings({"unchecked", "rawtypes"})
public class LocationServiceImpl implements LocationService {
    @Resource // 资源注入，用于获取Redis操作模板
    private RedisTemplate redisTemplate;// Redis操作模板，用于执行Redis命令
    @Resource
    private DriverInfoFeignClient driverInfoFeignClient;// 配送员信息Feign客户端，用于调用配送员服务
    @Resource
    private OrderServiceLocationRepository orderServiceLocationRepository;// 订单服务位置仓库，用于操作订单服务位置数据
    @Resource
    private MongoTemplate mongoTemplate;// MongoDB操作模板，用于执行MongoDB命令

    /**
     * 更新配送员位置信息到Redis地理空间索引
     *
     * @param updateDriverLocationForm 包含配送员位置信息的表单对象
     * @return 操作成功结果
     */
    @Override
    public Boolean updateDriverLocation(UpdateDriverLocationForm updateDriverLocationForm) {
        // 创建地理坐标点对象，包含经度和纬度
        Point point = new Point(updateDriverLocationForm.getLongitude().doubleValue(), updateDriverLocationForm.getLatitude().doubleValue());
        // 将配送员位置信息添加到Redis地理空间索引中
        redisTemplate.opsForGeo().add(RedisConstant.DRIVER_GEO_LOCATION, point, updateDriverLocationForm.getDriverId().toString());
        // 返回操作成功结果
        return true;
    }

    /**
     * 删除配送员位置信息
     *
     * @param driverId 配送员ID
     * @return 操作成功结果
     */
    @Override
    public Boolean removeDriverLocation(Long driverId) {
        // 从Redis地理空间索引中移除指定配送员的定位信息
        redisTemplate.opsForGeo().remove(RedisConstant.DRIVER_GEO_LOCATION, driverId.toString());
        // 返回操作成功结果
        return true;
    }

    /**
     * 查询附近配送员
     *
     * @param searchNearByDriverForm 包含查询条件的表单对象
     * @return 附近配送员列表
     */
    @Override
    public List<NearByDriverVo> searchNearByDriver(SearchNearByDriverForm searchNearByDriverForm) {
        // 创建查询中心点坐标
        Point point = new Point(searchNearByDriverForm.getLongitude().doubleValue(), searchNearByDriverForm.getLatitude().doubleValue());
        // 设置查询半径（5公里）,distance参数：查询半径、单位
        Distance distance = new Distance(SystemConstant.NEARBY_DRIVER_RADIUS, RedisGeoCommands.DistanceUnit.KILOMETERS);
        // 创建查询圆形区域
        Circle circle = new Circle(point, distance);
        // 设置查询参数：包含距离、坐标信息，并按距离升序排序
        /*
        RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs()
        创建一个新的地理半径查询参数构建器
        这是构建查询参数的起点，返回一个参数构建器实例

        .includeDistance()
        包含距离信息：在查询结果中包含每个匹配项与中心点的距离
        这样可以在结果中获取每个配送员距离查询点的实际公里数
        距离值会以Distance对象的形式返回，包含数值和单位

        .includeCoordinates()
        包含坐标信息：在查询结果中包含每个匹配项的经纬度坐标
        这样可以获取每个配送员的具体位置信息（经度和纬度）
        坐标会以Point对象的形式返回

        .sortAscending()
        按距离升序排序：将查询结果按照距离从近到远排序
        这样返回的列表中最前面的就是距离最近的配送员
        对于找附近配送员的场景非常实用，可以优先推荐最近的配送员
         */
        RedisGeoCommands.GeoRadiusCommandArgs geoRadiusCommandArgs = RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeDistance().includeCoordinates().sortAscending();
        // 执行Redis地理空间查询，获取指定半径内的配送员
        GeoResults<RedisGeoCommands.GeoLocation<String>> result = redisTemplate.opsForGeo().radius(RedisConstant.DRIVER_GEO_LOCATION, circle, geoRadiusCommandArgs);
        // 获取查询结果内容
        /*
        详细解释：
        result
        这是Redis地理空间查询的结果对象，类型为 GeoResults<RedisGeoCommands.GeoLocation<String>>
        包含了查询到的所有地理空间数据

        .getContent()
        获取查询结果的内容列表
        返回一个包含所有匹配项的列表

        List<GeoResult<RedisGeoCommands.GeoLocation<String>>>
        这是一个复杂的泛型类型，让我们逐层分解：
        类型分解：
        List<>：Java集合接口，表示一个有序的元素列表

        GeoResult<>：Spring Data Redis的地理结果包装类，包含：

        实际的地理位置数据
        距离信息（如果查询时设置了includeDistance）
        其他元数据

        RedisGeoCommands.GeoLocation<>：Redis地理位置数据类型，包含：
        name：地理位置名称（String类型，这里存储的是配送员ID）
        point：地理坐标点（包含经度和纬度）
        String：泛型参数，表示地理位置名称的类型，这里使用String存储配送员ID
         */
        List<GeoResult<RedisGeoCommands.GeoLocation<String>>> content = result.getContent();
        // 初始化附近配送员列表
        List<NearByDriverVo> list = new ArrayList<>();

        // 检查查询结果是否为空
        if (!CollectionUtils.isEmpty(content)) {
            // 遍历查询结果，标准方法调用，用于获取集合的迭代器对象。
            /*
            迭代器模式
            提供了一种标准的方式来顺序访问集合中的元素
            不需要了解集合的内部实现细节
            支持安全的遍历操作
             */
            Iterator<GeoResult<RedisGeoCommands.GeoLocation<String>>> iterator = content.iterator();
            // 遍历迭代器，处理每个查询结果项
            while (iterator.hasNext()) {
                // 返回迭代器中的下一个元素，并将迭代器指针向前移动一位
                GeoResult<RedisGeoCommands.GeoLocation<String>> item = iterator.next();
                // 获取配送员ID
                Long driverId = Long.valueOf(item.getContent().getName());
                // 计算并格式化当前距离（保留两位小数，四舍五入）
                BigDecimal currentDistance = new BigDecimal(item.getDistance().getValue()).setScale(2, RoundingMode.HALF_UP);
                // 调用配送员信息服务获取配送员详细信息
                DriverSet driverSet = driverInfoFeignClient.getDriverSet(driverId).getData();

                // 检查配送员接单距离限制：如果当前配送员位置与订单起始位置点的距离超过配送员自定义的接单里程距离，跳过该配送员
                if (driverSet.getAcceptDistance().doubleValue() != 0 && driverSet.getAcceptDistance().subtract(currentDistance).doubleValue() < 0) {
                    continue;
                }
                // 检查配送员订单距离限制：如果订单里程超过配送员自定义的订单距离限制，跳过该配送员
                if (driverSet.getOrderDistance().doubleValue() != 0 && driverSet.getOrderDistance().subtract(searchNearByDriverForm.getMileageDistance()).doubleValue() < 0) {
                    continue;
                }

                // 创建附近配送员视图对象
                NearByDriverVo nearByDriverVo = new NearByDriverVo();
                // 设置配送员ID
                nearByDriverVo.setDriverId(driverId);
                // 设置距离信息
                nearByDriverVo.setDistance(currentDistance);
                // 添加到结果列表
                list.add(nearByDriverVo);
            }
        }
        // 返回符合条件的附近配送员列表
        return list;
    }

    /**
     * 更新订单位置信息到Redis缓存
     * 该方法用于将订单的最新地理位置（经纬度）存储到Redis缓存中，便于快速获取
     *
     * @param updateOrderLocationForm 订单位置更新表单，包含订单ID和最新的经纬度信息
     * @return Boolean 操作结果，true表示成功将位置信息更新到缓存
     */
    @Override
    public Boolean updateOrderLocationToCache(UpdateOrderLocationForm updateOrderLocationForm) {
        // 创建订单位置信息视图对象，用于存储经纬度数据
        OrderLocationVo orderLocationVo = new OrderLocationVo();
        // 设置纬度信息，从表单中获取
        orderLocationVo.setLatitude(updateOrderLocationForm.getLatitude());
        // 设置经度信息，从表单中获取
        orderLocationVo.setLongitude(updateOrderLocationForm.getLongitude());

        // 将订单位置信息存入Redis缓存
        redisTemplate.opsForValue().set(RedisConstant.UPDATE_ORDER_LOCATION + updateOrderLocationForm.getOrderId(), orderLocationVo);

        // 返回操作成功标志
        return true;
    }

    /**
     * 从Redis缓存中获取订单位置信息
     * 该方法用于根据订单ID从Redis缓存中获取之前存储的订单位置信息（经纬度）
     *
     * @param orderId 订单ID，用于唯一标识要获取位置信息的订单
     * @return OrderLocationVo 订单位置信息视图对象，包含订单的经纬度信息
     */
    @Override
    public OrderLocationVo getCacheOrderLocation(Long orderId) {
        // 从Redis缓存中获取订单位置信息
        OrderLocationVo orderLocationVo = (OrderLocationVo) redisTemplate.opsForValue().get(RedisConstant.UPDATE_ORDER_LOCATION + orderId);
        // 返回获取到的订单位置信息
        return orderLocationVo;
    }

    /**
     * 保存订单服务位置信息
     *
     * @param orderLocationServiceFormList 订单服务位置表单列表，包含需要保存的位置信息
     * @return 操作是否成功
     */
    @Override
    public Boolean saveOrderServiceLocation(List<OrderServiceLocationForm> orderLocationServiceFormList) {
        // 创建订单服务位置实体列表，用于存储转换后的实体对象
        ArrayList<OrderServiceLocation> list = new ArrayList<>();

        // 遍历位置表单列表，将每个表单对象转换为实体对象
        orderLocationServiceFormList.forEach(orderLocationServiceForm -> {
            // 创建订单服务位置实体对象
            OrderServiceLocation orderServiceLocation = new OrderServiceLocation();
            // 复制表单属性到实体对象
            BeanUtils.copyProperties(orderLocationServiceForm, orderServiceLocation);
            // 生成唯一ID
            orderServiceLocation.setId(ObjectId.get().toString());
            // 设置创建时间为当前时间
            orderServiceLocation.setCreateTime(new Date());
            // 将转换后的实体对象添加到列表中
            list.add(orderServiceLocation);
        });

        // 批量保存所有订单服务位置信息
        orderServiceLocationRepository.saveAll(list);

        // 返回操作成功结果
        return true;
    }

    /**
     * 获取订单服务的最后位置信息
     *
     * @param orderId 订单ID，用于查询特定订单的位置信息
     * @return 订单服务的最后位置视图对象，包含最新的位置信息
     */
    @Override
    public OrderServiceLastLocationVo getOrderServiceLastLocation(Long orderId) {
        // 创建MongoDB查询对象
        Query query = new Query();
        // 添加查询条件：订单ID等于传入的订单ID
        query.addCriteria(Criteria.where("orderId").is(orderId));
        // 设置排序方式：按创建时间倒序排列，确保最新的记录排在前面
        query.with(Sort.by(Sort.Order.desc("createTime")));
        // 限制查询结果数量为1，只获取最新的一条记录
        query.limit(1);

        // 执行查询，获取符合条件的最新订单服务位置记录
        OrderServiceLocation orderServiceLocation = mongoTemplate.findOne(query, OrderServiceLocation.class);

        // 创建订单服务最后位置视图对象，用于返回给前端
        OrderServiceLastLocationVo orderServiceLastLocationVo = new OrderServiceLastLocationVo();
        // 将查询到的实体对象属性复制到视图对象中
        BeanUtils.copyProperties(orderServiceLocation, orderServiceLastLocationVo);

        // 返回订单服务的最后位置视图对象
        return orderServiceLastLocationVo;
    }

    /**
     * 计算订单的实际行驶距离
     * 通过订单的位置轨迹点，计算相邻位置之间的距离并累加，得到总行驶距离
     *
     * @param orderId 订单ID，用于查询该订单的位置轨迹
     * @return 计算得到的实际行驶距离（单位：公里），如果没有位置数据则返回默认值5公里
     */
    @Override
    public BigDecimal calculateOrderRealDistance(Long orderId) {
        // 根据订单ID查询该订单的所有服务位置，并按创建时间升序排序
        List<OrderServiceLocation> orderServiceLocationList = orderServiceLocationRepository.findByOrderIdOrderByCreateTimeAsc(orderId);

        // 初始化实际距离变量
        double realDistance = 0;
        // 检查位置列表是否为空
        if (!CollectionUtils.isEmpty(orderServiceLocationList)) {
            // 遍历位置列表，计算相邻位置之间的距离
            for (int i = 0; i < orderServiceLocationList.size() - 1; i++) {
                // 当前位置点
                OrderServiceLocation orderServiceLocation = orderServiceLocationList.get(i);
                // 下一个位置点
                OrderServiceLocation nextOrderServiceLocation = orderServiceLocationList.get(i + 1);
                // 使用LocationUtil工具类计算两个经纬度点之间的距离
                double distance = LocationUtil.getDistance(orderServiceLocation.getLatitude().doubleValue(), orderServiceLocation.getLongitude().doubleValue(),
                        nextOrderServiceLocation.getLatitude().doubleValue(), nextOrderServiceLocation.getLongitude().doubleValue());
                // 累加到总距离
                realDistance += distance;
            }
        }

        // 如果没有位置数据或计算出的距离为0，返回默认值5公里
        if (realDistance == 0) {
            return new BigDecimal(5);
        }
        // 将计算得到的距离转换为BigDecimal并返回
        return new BigDecimal(realDistance);
    }
}
